package org.swing;

import com.jgoodies.validation.*;
import com.jgoodies.validation.view.ValidationComponentUtils;

import javax.swing.*;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Vector;

/**
 * Created by babitsky.viacheslav
 * Date: 01.02.2008
 * Time: 20:57:21
 */
final public class JExtNumberTextEditor extends JTextField implements Validatable {
    private static final long serialVersionUID = 7088026784037226679L;

    private static final char DOT = '.';

    private static final char NEGATIVE = '-';

    private static final String BLANK = "";

    private static final int DEF_PRECISION = 2;

    public static final String FM_NUMERIC = "0123456789";

    public static final String FM_DECIMAL = FM_NUMERIC + DOT;

    private int maxLength = 0;

    private NumberType type;

    private String negativeChars = BLANK;

    private String allowedChars = null;

    private boolean allowNegative = false;

    private int precision = 0;

    protected PlainDocument numberFieldFilter;
    private IconFeedbackPanel iconFeedbackPanel;
    private Validator validator;
    private ValidationType validationType;
    private final Vector<ValidationMessage> validationMessageVector = new Vector<ValidationMessage>();
    private int serialValue;
    private PropertyChangeListener propertyChangeListener;
    private BigDecimal bigDecimalValue;

    public JExtNumberTextEditor() {
        super();
        serialValue = ValidationComponentUtils.getNextSerialValue();
        ValidationComponentUtils.setMessageKey(this, serialValue);
        setMaxLength(20);
        setFormat(NumberType.INTEGER);
        setHorizontalAlignment(JTextField.RIGHT);
        numberFieldFilter = new JNumberFieldFilter();
        super.setDocument(numberFieldFilter);
        addPropertyChangeListener("text", new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent evt) {
                BigDecimal old = bigDecimalValue;
                bigDecimalValue = getValue();
                firePropertyChange("value", old, bigDecimalValue);
            }
        });
    }

    public void setMaxLength(int maxLen) {
        if (maxLen > 0) {
            maxLength = maxLen;
            setColumns(maxLen);
        } else
            maxLength = 0;
    }

    public int getMaxLength() {
        return maxLength;
    }

    public void setPrecision(int iPrecision) {
        if (type == NumberType.INTEGER)
            return;

        if (iPrecision >= 0)
            precision = iPrecision;
        else
            precision = DEF_PRECISION;
    }

    public int getPrecision() {
        return precision;
    }

    public void setValue(BigDecimal value) {
        if (value == null) setText("");
        else {
            if (value.signum() == -1 && !isAllowNegative())
                throw new IllegalArgumentException("value is less than zero");
            if (value.scale() > precision) value = value.setScale(precision, BigDecimal.ROUND_HALF_UP);
            setText(value.toString());
        }
    }

    public void setValue(Integer value) {
        if (value == null) setText("");
        else {
            if (value < 0 && !isAllowNegative()) throw new IllegalArgumentException("value is less than zero");
            setText(value.toString());
        }
    }

    public void setValue(Short value) {
        if (value == null) setText("");
        else {
            if (value < 0 && !isAllowNegative()) throw new IllegalArgumentException("value is less than zero");
            setText(value.toString());
        }
    }

    public Integer getInteger() {
        if (isEmpty()) return null;
        try {
            return new Integer(getText());
        } catch (Exception e) {
            return null;
        }

    }

    public BigDecimal getValue() {
        if (isEmpty()) return null;
        try {
            return new BigDecimal(getText());
        } catch (Exception e) {
            return null;
        }
    }

    public Short getShort() {
        if (isEmpty()) return null;
        try {
            return new Short(getText());
        } catch (Exception e) {
            return null;
        }
    }

    public NumberType getFormat() {
        return type;
    }

    public void setFormat(NumberType iType) {
        if (NumberType.INTEGER == iType) {
            type = NumberType.INTEGER;
            precision = 0;
            allowedChars = FM_NUMERIC;
        } else {
            type = NumberType.FLOAT;
            precision = DEF_PRECISION;
            allowedChars = FM_DECIMAL;
        }
    }

    public void setAllowNegative(boolean b) {
        allowNegative = b;

        if (b)
            negativeChars = "" + NEGATIVE;
        else
            negativeChars = BLANK;
    }

    public boolean isAllowNegative() {
        return allowNegative;
    }


    public void setDocument(Document document) {
    }

    private boolean isEmpty() {
        return getText() == null || getText().length() == 0;
    }

    public void addValidater(IconFeedbackPanel iconFeedbackPanel, Validator validator, ValidationType validationType) {
        this.iconFeedbackPanel = iconFeedbackPanel;
        this.validator = validator;
        this.validationType = validationType;
        iconFeedbackPanel.registerComponent(this);
        if (validationType == validateOnValueChanged) {
            propertyChangeListener = new PropertyChangeListener() {
                public void propertyChange(PropertyChangeEvent evt) {
                    if (JExtNumberTextEditor.this.getParent() != null) {
                        JExtNumberTextEditor.this.iconFeedbackPanel.setOwnMessagesAndRevalidateOtherRegistredComponent(getSerialValue(), validateValue(), false);
                    }
                }
            };
            addPropertyChangeListener("value", propertyChangeListener);
        }
    }

    public void removeValidater() {
        validator = null;
        validationType = null;
        if (propertyChangeListener != null) {
            removePropertyChangeListener(propertyChangeListener);
            propertyChangeListener = null;
        }
    }

    /**
     * Validates this object and returns a {@link com.jgoodies.validation.ValidationResult}.
     *
     * @return the validation result
     */
    public Vector<ValidationMessage> validateValue() {
        if (validator != null) {
            return validator.validate(getValue(), serialValue);
        }
        return validationMessageVector;
    }

    public int getSerialValue() {
        return serialValue;
    }

    public void setMandarory(boolean mandatory) {
        ValidationComponentUtils.setMandatory(this, mandatory);
    }

    public boolean isMandatory() {
        return ValidationComponentUtils.isMandatory(this);
    }

    /**
     * Processes focus events occurring on this component by
     * dispatching them to any registered
     * <code>FocusListener</code> objects.
     * <p/>
     * This method is not called unless focus events are
     * enabled for this component. Focus events are enabled
     * when one of the following occurs:
     * <p><ul>
     * <li>A <code>FocusListener</code> object is registered
     * via <code>addFocusListener</code>.
     * <li>Focus events are enabled via <code>enableEvents</code>.
     * </ul>
     * <p/>
     * If focus events are enabled for a <code>Component</code>,
     * the current <code>KeyboardFocusManager</code> determines
     * whether or not a focus event should be dispatched to
     * registered <code>FocusListener</code> objects.  If the
     * events are to be dispatched, the <code>KeyboardFocusManager</code>
     * calls the <code>Component</code>'s <code>dispatchEvent</code>
     * method, which results in a call to the <code>Component</code>'s
     * <code>processFocusEvent</code> method.
     * <p/>
     * If focus events are enabled for a <code>Component</code>, calling
     * the <code>Component</code>'s <code>dispatchEvent</code> method
     * with a <code>FocusEvent</code> as the argument will result in a
     * call to the <code>Component</code>'s <code>processFocusEvent</code>
     * method regardless of the current <code>KeyboardFocusManager</code>.
     * <p/>
     * <p>Note that if the event parameter is <code>null</code>
     * the behavior is unspecified and may result in an
     * exception.
     *
     * @param e the focus event
     * @see java.awt.event.FocusEvent
     * @see java.awt.event.FocusListener
     * @see java.awt.KeyboardFocusManager
     * @see #addFocusListener
     * @see #enableEvents
     * @see #dispatchEvent
     * @since JDK1.1
     */
    @Override
    protected void processFocusEvent(FocusEvent e) {
        super.processFocusEvent(e);
        if (e.getID() == FocusEvent.FOCUS_LOST) {
            if (validationType == validateOnLostFocus) {
                if (getParent() != null) {
                    iconFeedbackPanel.setOwnMessagesAndRevalidateOtherRegistredComponent(getSerialValue(), validateValue(), true);
                }
            }
        }
    }

    class JNumberFieldFilter extends PlainDocument {

        private static final long serialVersionUID = 1666437615495357051L;
        private boolean negatiateFlag = false;

        public JNumberFieldFilter() {
            super();
        }

        public void insertString(int offset, String str, AttributeSet attr) throws BadLocationException {
            String oldValue = getText(0, getLength());
            if (str == null)
                return;

            String text = getText(0, offset) + str + getText(offset, (getLength() - offset));
            if (str.length() == 1 && DOT == str.charAt(0)) {
                if (offset >= 0 && offset < getLength() && getText(offset, 1).charAt(0) == DOT) {
                    replace(offset, 1, str, attr);
                    return;
                }
            }
            if (str.length() == 1 && Character.isDigit(str.charAt(0))) {
                int pos_dot = getText(0, getLength()).indexOf(DOT);
                if (offset >= 0 && offset < getLength() && pos_dot >= 0 && pos_dot < offset && getLength() - pos_dot == precision + 1) {
                    replace(offset, 1, str, attr);
                    return;
                }
            }

            if ("-".equals(str)) {
                if (getLength() == 0) {
                    negatiateFlag = allowNegative;
                } else {
                    if (getText(0, getLength()).startsWith(str)) {
                        remove(0, 1);
                        return;
                    }
                    offset = 0;
                    text = str + getText(offset, (getLength() - offset));
                }
            } else {
                if (!text.startsWith("-") && offset == 0 && negatiateFlag) str = "-" + str;
                negatiateFlag = false;
            }

            for (int i = 0; i < str.length(); i++) {
                if ((allowedChars + negativeChars).indexOf(str.charAt(i)) == -1)
                    return;
            }

            int precisionLength = 0, dotLength = 0, minusLength = 0;
            int textLength = text.length();

            try {
                if (type == NumberType.INTEGER) {
                    new BigInteger(text);
                } else if (type == NumberType.FLOAT) {
                    new BigDecimal(text);

                    int dotIndex = text.indexOf(DOT);
                    if (dotIndex != -1) {
                        dotLength = 1;
                        precisionLength = textLength - dotIndex - dotLength;

                        if (precisionLength > precision)
                            return;
                    }
                }
            }
            catch (Exception ex) {
                return;
            }

            if (text.startsWith("" + NEGATIVE)) {
                if (!allowNegative)
                    return;
                else
                    minusLength = 1;
            }

            if (maxLength < (textLength - dotLength - precisionLength - minusLength)){
                if (str.length() == 1 && Character.isDigit(str.charAt(0))) replace(offset, 1, str, attr);
                return;
            }

            super.insertString(offset, str, attr);
            String newValue = getText(0, getLength());
            firePropertyChange("text", oldValue, newValue);
        }

        /**
         * Removes some content from the document.
         * Removing content causes a write lock to be held while the
         * actual changes are taking place.  Observers are notified
         * of the change on the thread that called this method.
         * <p/>
         * This method is thread safe, although most Swing methods
         * are not. Please see
         * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
         * to Use Threads</A> for more information.
         *
         * @param offs the starting offset >= 0
         * @param len  the number of characters to remove >= 0
         * @throws javax.swing.text.BadLocationException
         *          the given remove position is not a valid
         *          position within the document
         * @see javax.swing.text.Document#remove
         */
        @Override
        public void remove(int offs, int len) throws BadLocationException {
            String oldValue = null;
            try {
                oldValue = getText(0, getLength());
            } catch (BadLocationException e) {
                e.printStackTrace();
            }
            super.remove(offs, len);
            String newValue = null;
            try {
                newValue = getText(0, getLength());
            } catch (BadLocationException e) {
                e.printStackTrace();
            }
            firePropertyChange("text", oldValue, newValue);
        }


    }

    public enum NumberType {
        INTEGER,
        FLOAT
    }

}
